<!DOCTYPE html>
<meta charset="UTF-8">
<link href="./smartMenu/smartMenu.css" rel="stylesheet">
<style>
  .node circle {
    fill: #fff;
    stroke: steelblue;
    stroke-width: 3px;
  }

  .node text {
    font: 12px sans-serif;
  }

  .link {
    fill: none;
    stroke: #ccc;
    stroke-width: 4px;
    cursor: pointer;
  }

  .chartTooltip {
    position: absolute;
    width: 200px;
    height: auto;
    padding: 10px;
    box-sizing: border-box;
    background-color: white;
    border-radius: 5px;
    box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.4);
    pointer-events: none;
  }

  .chartTooltip.hidden {
    display: none;
  }

  .chartTooltip p {
    margin: 0;
    font-size: 14px;
    line-height: 20px;
    word-wrap: break-word;
  }
</style>

<body>
  <div class="chartTooltip hidden">
    <p>
      <strong class="name"></strong>
    </p>
  </div>
  <pre style="font-size: 24px;">
      Click to toggle child nodes. 
      Double click to ajax child nodes data. 
      Ctrl+wheel to zoom scale. 
      Right-click to show menu. 
      Showing info panel while mouseover node. 
      Changing link color while mouseover link. 
  </pre>
  <!-- load the d3.js library -->
  <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>
  <script src="./smartMenu/smartMenu.js"></script>
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <script>
    // Original data.
    var treeData = {
      name: "Top Level"
    };

    /********************* 1. Init D3 setting *********************/
    var margin = {
        top: 20,
        right: 90,
        bottom: 30,
        left: 90
      },
      width = 1220 - margin.left - margin.right,
      height = 900 - margin.top - margin.bottom;

    var svg = d3
      .select("body")
      .append("svg")
      .attr("width", width + margin.right + margin.left)
      .attr("height", height + margin.top + margin.bottom);

    var view = svg
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // Create zoom operation
    var zoom = d3
      .zoom()
      .scaleExtent([0.1, 100])
      .on("zoom", () => {
        view.attr(
          "transform",
          "translate(" +
          (d3.event.transform.x + margin.left) +
          "," +
          (d3.event.transform.y + margin.top) +
          ") scale(" +
          d3.event.transform.k +
          ")"
        );
      });

    // Disable double click event of zoom
    svg.call(zoom).on("dblclick.zoom", () => {});

    var i = 0,
      duration = 750,
      root;

    var userMenuData = [
      [{
          text: "Menu1",
          func: function () {
            var id = Number($(this).attr("id"))
            alert("Menu1", id)
          }
        },
        {
          text: "Menu2",
          func: function () {
            var id = Number($(this).attr("id"))
            alert("Menu1", id)
          }
        }
      ]
    ];
    // Add right click menu.
    $("body").smartMenu(userMenuData, {
      name: "chatRightControl",
      container: "g.node"
    });

    initData();

    /********************* 2. Data binding init *********************/
    function initData() {
      root = d3.hierarchy(treeData, function (d) {
        return d.children;
      });
      root.x0 = height / 2;
      root.y0 = 0;

      // Update node status
      updateChart(root);
    }

    /********************* 3. Data binding update  *********************/
    function updateChart(source) {
      // Compute the scale of chart, while the nodes generate from ajax data each time.
      var scale =
        (getDepth(root) / 8 || 0.5) +
        (getMax(root) / 12 || 0.5);
      // chartHeight = height * scale
      var treemap = d3.tree().size([height * scale, width]);
      
      var treeData = treemap(root);

      var nodes = treeData.descendants(),
        links = treeData.descendants().slice(1);

      nodes.forEach(function (d) {
        d.y = d.depth * 180;
      });

      updateNodes(source, nodes);
      updateLinks(source, links);

      nodes.forEach(function (d) {
        d.x0 = d.x;
        d.y0 = d.y;
      });

    }


    /********************* 4. Draw and update nodes  *********************/
    function updateNodes(source, nodes) {
      var node = view.selectAll("g.node").data(nodes, function (d) {
        return d.id || (d.id = ++i);
      });

      var nodeEnter = node
        .enter()
        .append("g")
        .attr("class", "node")
        .attr("id", d => d.id)
        .attr("transform", function (d) {
          return "translate(" + source.y0 + "," + source.x0 + ")";
        })
        .on("mouseover", d => {
          var transform = d3.event;
          var yPosition = transform.offsetY + 20;
          var xPosition = transform.offsetX + 20;

          var chartTooltip = d3
            .select(".chartTooltip")
            .style("left", xPosition + "px")
            .style("top", yPosition + "px");

          chartTooltip.select(".name").text(d.data.name);

          chartTooltip.classed("hidden", false);
        })
        .on("mouseout", () => {
          d3.select(".chartTooltip").classed("hidden", true);
        })
        .on("click", click)
        .on("dblclick", dblclick);

      nodeEnter
        .append("circle")
        .attr("class", "node")
        .attr("r", 1e-6)
        .style("fill", function (d) {
          return d._children ? "lightsteelblue" : d.data.children ? "#fff" : "#aaa";
        });

      nodeEnter
        .append("text")
        .attr("dy", ".35em")
        .attr("x", function (d) {
          return d.children || d._children ? -13 : 13;
        })
        .attr("text-anchor", function (d) {
          return d.children || d._children ? "end" : "start";
        })
        .text(function (d) {
          return d.data.name;
        });

      var nodeUpdate = nodeEnter.merge(node);

      nodeUpdate
        .transition()
        .duration(duration)
        .attr("transform", function (d) {
          return "translate(" + d.y + "," + d.x + ")";
        });

      nodeUpdate
        .select("circle.node")
        .attr("r", 10)
        .style("fill", function (d) {
          return d._children ? "lightsteelblue" : d.data.children ? "#fff" : "#aaa";
        })
        .attr("cursor", "pointer");

      var nodeExit = node
        .exit()
        .transition()
        .duration(duration)
        .attr("transform", function (d) {
          return "translate(" + source.y + "," + source.x + ")";
        })
        .remove();

      nodeExit.select("circle").attr("r", 1e-6);

      nodeExit.select("text").style("fill-opacity", 1e-6);
    }



    /********************* 5. Draw and update links  *********************/
    function updateLinks(source, links) {
      var link = view.selectAll("path.link").data(links, function (d) {
        return d.id;
      });

      var linkEnter = link
        .enter()
        .insert("path", "g")
        .attr("class", "link")
        .attr("id", d => {
          return "textPath" + d.id;
        })
        .on("mouseover", function(d){
          d3.select(this).style("stroke", "orange");
        })
        .on("mouseout", function(d){
          d3.select(this).style("stroke", '#CCC');
        })
        .on("click", d => {
          alert(d.parent.data.name + ' -> ' + d.data.name);
        })
        .attr("d", function (d) {
          var o = {
            x: source.x0,
            y: source.y0
          };
          return diagonalReverse(o, o);
        });

      link
        .enter()
        .append("text")
        .append("textPath") 
        .attr("xlink:href", d => {
          return "#textPath" + d.id;
        }) 
        .style("text-anchor", "middle")
        .attr("startOffset", "50%")
        .style("fill", "red")
        .text(function (d) {
          return d.parent.id;
        })
        .append("tspan")
        .style("fill", "blue")
        .text(' --> ')
        .append("tspan")
        .style("fill", "red")
        .text(function (d) {
          return d.id;
        });

      var linkUpdate = linkEnter.merge(link);

      linkUpdate
        .transition()
        .duration(duration)
        .attr("d", function (d) {
          return diagonalReverse(d, d.parent);
        });

      var linkExit = link
        .exit()
        .transition()
        .duration(duration)
        .attr("d", function (d) {
          var o = {
            x: source.x,
            y: source.y
          };
          return diagonalReverse(o, o);
        })
        .remove();

    }


    /********************* 6. Click event  *********************/
    function click(d) {
      // Seperate dbclick from click.
      if (d._clickid) {
        clearTimeout(d._clickid);
        d._clickid = null;
      } else {
        d._clickid = setTimeout(() => {
          if (d.children) {
            d._children = d.children;
            d.children = null;
          } else {
            d.children = d._children;
            d._children = null;
          }
          updateChart(d);
          d._clickid = null;
        }, 350);
      }
    }



    /********************* 7. 双击获取子节点事件处理  *********************/
    // 将获取到的节点，添加进data对象中，同时若已获取过不再获取
    function dblclick(d) {
      // 若无d.data.children，则视为未获取
      if (!(d.data && d.data.children)) {
        // 这里模拟请求，1.json - 5.json 随机获取数据
        var randomNum = Math.floor(Math.random() * 5) + 1;
        d3.json(randomNum + ".json", function (error, data) {
          if (error) throw error;
          // 给子节点绑定父节点
          var children = data.children.map(x => {
            return {
              name: x.name,
              parent: d,
              depth: d.depth + 1,
              data: {
                ...x
              }
            }
          });
          // 将子节点数据绑定在d节点上
          if (children.length) d.children = children;
          // 同时也绑到data上
          d.data.children = children;
          updateChart(d);
        });
      }
    }


    // collapse方法，切换子节点展开收起
    function collapse(d) {
      if (d.children) {
        d._children = d.children;
        d._children.forEach(collapse);
        d.children = null;
      }
    }

    // 添加贝塞尔曲线的path，衔接与父节点和子节点间
    function diagonal(s, d) {
      path =
        `M ${s.y} ${s.x}
              C ${(s.y + d.y) / 2} ${s.x},
                ${(s.y + d.y) / 2} ${d.x},
                ${d.y} ${d.x}`;

      return path;
    }

    // 添加贝塞尔曲线的path，方向为父节点指向子节点
    function diagonalReverse(s = {}, d = {}) {
      path =
      `M ${d.y} ${d.x}
                  C ${(s.y + d.y) / 2} ${d.x},
                  ${(s.y + d.y) / 2} ${s.x},
                  ${s.y} ${s.x}`;
        return path;
    }

    // 获取最多的子节点数
    function getMax(obj) {
      let max = 0;
      if (obj.children) {
        max = obj.children.length;
        obj.children.forEach(d => {
          const tmpMax = this.getMax(d);
          if (tmpMax > max) {
            max = tmpMax;
          }
        });
      }
      return max;
    }

    // Get node Depth
    function getDepth(obj) {
      var depth = 0;
      if (obj.children) {
        obj.children.forEach(d => {
          var tmpDepth = this.getDepth(d);
          if (tmpDepth > depth) {
            depth = tmpDepth;
          }
        });
      }
      return 1 + depth;
    }
  </script>
</body>